/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.gallery3d.glrenderer;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;

import com.android.gallery3d.util.IntArray;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;

public class GLES20Canvas implements GLCanvas {
	// ************** Constants **********************
	private static final String TAG = GLES20Canvas.class.getSimpleName();
	private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE;
	private static final float OPAQUE_ALPHA = 0.95f;

	private static final int COORDS_PER_VERTEX = 2;
	private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE;

	private static final int COUNT_FILL_VERTEX = 4;
	private static final int COUNT_LINE_VERTEX = 2;
	private static final int COUNT_RECT_VERTEX = 4;
	private static final int OFFSET_FILL_RECT = 0;
	private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT
			+ COUNT_FILL_VERTEX;
	private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE
			+ COUNT_LINE_VERTEX;

	private static final float[] BOX_COORDINATES = { 0, 0, // Fill rectangle
			1, 0, 0, 1, 1, 1, 0, 0, // Draw line
			1, 1, 0, 0, // Draw rectangle outline
			0, 1, 1, 1, 1, 0, };

	private static final float[] BOUNDS_COORDINATES = { 0, 0, 0, 1, 1, 1, 0, 1, };

	private static final String POSITION_ATTRIBUTE = "aPosition";
	private static final String COLOR_UNIFORM = "uColor";
	private static final String MATRIX_UNIFORM = "uMatrix";
	private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix";
	private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler";
	private static final String ALPHA_UNIFORM = "uAlpha";
	private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate";

	private static final String DRAW_VERTEX_SHADER = "" + "uniform mat4 "
			+ MATRIX_UNIFORM + ";\n" + "attribute vec2 " + POSITION_ATTRIBUTE
			+ ";\n" + "void main() {\n" + "  vec4 pos = vec4("
			+ POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + "  gl_Position = "
			+ MATRIX_UNIFORM + " * pos;\n" + "}\n";

	private static final String DRAW_FRAGMENT_SHADER = ""
			+ "precision mediump float;\n" + "uniform vec4 " + COLOR_UNIFORM
			+ ";\n" + "void main() {\n" + "  gl_FragColor = " + COLOR_UNIFORM
			+ ";\n" + "}\n";

	private static final String TEXTURE_VERTEX_SHADER = "" + "uniform mat4 "
			+ MATRIX_UNIFORM + ";\n" + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM
			+ ";\n" + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
			+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
			+ "  vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
			+ "  gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
			+ "  vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n"
			+ "}\n";

	private static final String MESH_VERTEX_SHADER = "" + "uniform mat4 "
			+ MATRIX_UNIFORM + ";\n" + "attribute vec2 " + POSITION_ATTRIBUTE
			+ ";\n" + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n"
			+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
			+ "  vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
			+ "  gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
			+ "  vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" + "}\n";

	private static final String TEXTURE_FRAGMENT_SHADER = ""
			+ "precision mediump float;\n" + "varying vec2 vTextureCoord;\n"
			+ "uniform float " + ALPHA_UNIFORM + ";\n" + "uniform sampler2D "
			+ TEXTURE_SAMPLER_UNIFORM + ";\n" + "void main() {\n"
			+ "  gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM
			+ ", vTextureCoord);\n" + "  gl_FragColor *= " + ALPHA_UNIFORM
			+ ";\n" + "}\n";

	private static final String OES_TEXTURE_FRAGMENT_SHADER = ""
			+ "#extension GL_OES_EGL_image_external : require\n"
			+ "precision mediump float;\n" + "varying vec2 vTextureCoord;\n"
			+ "uniform float " + ALPHA_UNIFORM + ";\n"
			+ "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n"
			+ "void main() {\n" + "  gl_FragColor = texture2D("
			+ TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n"
			+ "  gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + "}\n";

	private static final int INITIAL_RESTORE_STATE_SIZE = 8;
	private static final int MATRIX_SIZE = 16;

	// Keep track of restore state
	private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE
			* MATRIX_SIZE];
	private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE];
	private IntArray mSaveFlags = new IntArray();

	private int mCurrentAlphaIndex = 0;
	private int mCurrentMatrixIndex = 0;

	// Viewport size
	private int mWidth;
	private int mHeight;

	// Projection matrix
	private float[] mProjectionMatrix = new float[MATRIX_SIZE];

	// Screen size for when we aren't bound to a texture
	private int mScreenWidth;
	private int mScreenHeight;

	// GL programs
	private int mDrawProgram;
	private int mTextureProgram;
	private int mOesTextureProgram;
	private int mMeshProgram;

	// GL buffer containing BOX_COORDINATES
	private int mBoxCoordinates;

	// Handle indices -- common
	private static final int INDEX_POSITION = 0;
	private static final int INDEX_MATRIX = 1;

	// Handle indices -- draw
	private static final int INDEX_COLOR = 2;

	// Handle indices -- texture
	private static final int INDEX_TEXTURE_MATRIX = 2;
	private static final int INDEX_TEXTURE_SAMPLER = 3;
	private static final int INDEX_ALPHA = 4;

	// Handle indices -- mesh
	private static final int INDEX_TEXTURE_COORD = 2;

	private abstract static class ShaderParameter {
		public int handle;
		protected final String mName;

		public ShaderParameter(String name) {
			mName = name;
		}

		public abstract void loadHandle(int program);
	}

	private static class UniformShaderParameter extends ShaderParameter {
		public UniformShaderParameter(String name) {
			super(name);
		}

		@Override
		public void loadHandle(int program) {
			handle = GLES20.glGetUniformLocation(program, mName);
			checkError();
		}
	}

	private static class AttributeShaderParameter extends ShaderParameter {
		public AttributeShaderParameter(String name) {
			super(name);
		}

		@Override
		public void loadHandle(int program) {
			handle = GLES20.glGetAttribLocation(program, mName);
			checkError();
		}
	}

	ShaderParameter[] mDrawParameters = {
			new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
			new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
			new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR
	};
	ShaderParameter[] mTextureParameters = {
			new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
			new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
			new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
			new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
			new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
	};
	ShaderParameter[] mOesTextureParameters = {
			new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
			new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
			new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
			new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
			new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
	};
	ShaderParameter[] mMeshParameters = {
			new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
			new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
			new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD
			new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
			new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
	};

	private final IntArray mUnboundTextures = new IntArray();
	private final IntArray mDeleteBuffers = new IntArray();

	// Keep track of statistics for debugging
	private int mCountDrawMesh = 0;
	private int mCountTextureRect = 0;
	private int mCountFillRect = 0;
	private int mCountDrawLine = 0;

	// Buffer for framebuffer IDs -- we keep track so we can switch the attached
	// texture.
	private int[] mFrameBuffer = new int[1];

	// Bound textures.
	private ArrayList<RawTexture> mTargetTextures = new ArrayList<RawTexture>();

	// Temporary variables used within calculations
	private final float[] mTempMatrix = new float[32];
	private final float[] mTempColor = new float[4];
	private final RectF mTempSourceRect = new RectF();
	private final RectF mTempTargetRect = new RectF();
	private final float[] mTempTextureMatrix = new float[MATRIX_SIZE];
	private final int[] mTempIntArray = new int[1];

	private static final GLId mGLId = new GLES20IdImpl();

	public GLES20Canvas() {
		Matrix.setIdentityM(mTempTextureMatrix, 0);
		Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
		mAlphas[mCurrentAlphaIndex] = 1f;
		mTargetTextures.add(null);

		FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES);
		mBoxCoordinates = uploadBuffer(boxBuffer);

		int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
				DRAW_VERTEX_SHADER);
		int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
				TEXTURE_VERTEX_SHADER);
		int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
				MESH_VERTEX_SHADER);
		int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
				DRAW_FRAGMENT_SHADER);
		int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
				TEXTURE_FRAGMENT_SHADER);
		int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
				OES_TEXTURE_FRAGMENT_SHADER);

		mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader,
				mDrawParameters);
		mTextureProgram = assembleProgram(textureVertexShader,
				textureFragmentShader, mTextureParameters);
		mOesTextureProgram = assembleProgram(textureVertexShader,
				oesTextureFragmentShader, mOesTextureParameters);
		mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader,
				mMeshParameters);
		GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
		checkError();
	}

	private static FloatBuffer createBuffer(float[] values) {
		// First create an nio buffer, then create a VBO from it.
		int size = values.length * FLOAT_SIZE;
		FloatBuffer buffer = ByteBuffer.allocateDirect(size)
				.order(ByteOrder.nativeOrder()).asFloatBuffer();
		buffer.put(values, 0, values.length).position(0);
		return buffer;
	}

	private int assembleProgram(int vertexShader, int fragmentShader,
			ShaderParameter[] params) {
		int program = GLES20.glCreateProgram();
		checkError();
		if (program == 0) {
			throw new RuntimeException("Cannot create GL program: "
					+ GLES20.glGetError());
		}
		GLES20.glAttachShader(program, vertexShader);
		checkError();
		GLES20.glAttachShader(program, fragmentShader);
		checkError();
		GLES20.glLinkProgram(program);
		checkError();
		int[] mLinkStatus = mTempIntArray;
		GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0);
		if (mLinkStatus[0] != GLES20.GL_TRUE) {
			Log.e(TAG, "Could not link program: ");
			Log.e(TAG, GLES20.glGetProgramInfoLog(program));
			GLES20.glDeleteProgram(program);
			program = 0;
		}
		for (int i = 0; i < params.length; i++) {
			params[i].loadHandle(program);
		}
		return program;
	}

	private static int loadShader(int type, String shaderCode) {
		// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
		// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
		int shader = GLES20.glCreateShader(type);

		// add the source code to the shader and compile it
		GLES20.glShaderSource(shader, shaderCode);
		checkError();
		GLES20.glCompileShader(shader);
		checkError();

		return shader;
	}

	@Override
	public void setSize(int width, int height) {
		mWidth = width;
		mHeight = height;
		GLES20.glViewport(0, 0, mWidth, mHeight);
		checkError();
		Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
		Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1);
		if (getTargetTexture() == null) {
			mScreenWidth = width;
			mScreenHeight = height;
			Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0);
			Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1);
		}
	}

	@Override
	public void clearBuffer() {
		GLES20.glClearColor(0f, 0f, 0f, 1f);
		checkError();
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
		checkError();
	}

	@Override
	public void clearBuffer(float[] argb) {
		GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]);
		checkError();
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
		checkError();
	}

	@Override
	public float getAlpha() {
		return mAlphas[mCurrentAlphaIndex];
	}

	@Override
	public void setAlpha(float alpha) {
		mAlphas[mCurrentAlphaIndex] = alpha;
	}

	@Override
	public void multiplyAlpha(float alpha) {
		setAlpha(getAlpha() * alpha);
	}

	@Override
	public void translate(float x, float y, float z) {
		Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z);
	}

	// This is a faster version of translate(x, y, z) because
	// (1) we knows z = 0, (2) we inline the Matrix.translateM call,
	// (3) we unroll the loop
	@Override
	public void translate(float x, float y) {
		int index = mCurrentMatrixIndex;
		float[] m = mMatrices;
		m[index + 12] += m[index + 0] * x + m[index + 4] * y;
		m[index + 13] += m[index + 1] * x + m[index + 5] * y;
		m[index + 14] += m[index + 2] * x + m[index + 6] * y;
		m[index + 15] += m[index + 3] * x + m[index + 7] * y;
	}

	@Override
	public void scale(float sx, float sy, float sz) {
		Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz);
	}

	@Override
	public void rotate(float angle, float x, float y, float z) {
		if (angle == 0f) {
			return;
		}
		float[] temp = mTempMatrix;
		Matrix.setRotateM(temp, 0, angle, x, y, z);
		float[] matrix = mMatrices;
		int index = mCurrentMatrixIndex;
		Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0);
		System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE);
	}

	@Override
	public void multiplyMatrix(float[] matrix, int offset) {
		float[] temp = mTempMatrix;
		float[] currentMatrix = mMatrices;
		int index = mCurrentMatrixIndex;
		Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset);
		System.arraycopy(temp, 0, currentMatrix, index, 16);
	}

	@Override
	public void save() {
		save(SAVE_FLAG_ALL);
	}

	@Override
	public void save(int saveFlags) {
		boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
		if (saveAlpha) {
			float currentAlpha = getAlpha();
			mCurrentAlphaIndex++;
			if (mAlphas.length <= mCurrentAlphaIndex) {
				mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2);
			}
			mAlphas[mCurrentAlphaIndex] = currentAlpha;
		}
		boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
		if (saveMatrix) {
			int currentIndex = mCurrentMatrixIndex;
			mCurrentMatrixIndex += MATRIX_SIZE;
			if (mMatrices.length <= mCurrentMatrixIndex) {
				mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2);
			}
			System.arraycopy(mMatrices, currentIndex, mMatrices,
					mCurrentMatrixIndex, MATRIX_SIZE);
		}
		mSaveFlags.add(saveFlags);
	}

	@Override
	public void restore() {
		int restoreFlags = mSaveFlags.removeLast();
		boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
		if (restoreAlpha) {
			mCurrentAlphaIndex--;
		}
		boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
		if (restoreMatrix) {
			mCurrentMatrixIndex -= MATRIX_SIZE;
		}
	}

	@Override
	public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
		draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1,
				x2 - x1, y2 - y1, paint);
		mCountDrawLine++;
	}

	@Override
	public void drawRect(float x, float y, float width, float height,
			GLPaint paint) {
		draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y,
				width, height, paint);
		mCountDrawLine++;
	}

	private void draw(int type, int offset, int count, float x, float y,
			float width, float height, GLPaint paint) {
		draw(type, offset, count, x, y, width, height, paint.getColor(),
				paint.getLineWidth());
	}

	private void draw(int type, int offset, int count, float x, float y,
			float width, float height, int color, float lineWidth) {
		prepareDraw(offset, color, lineWidth);
		draw(mDrawParameters, type, count, x, y, width, height);
	}

	private void prepareDraw(int offset, int color, float lineWidth) {
		GLES20.glUseProgram(mDrawProgram);
		checkError();
		if (lineWidth > 0) {
			GLES20.glLineWidth(lineWidth);
			checkError();
		}
		float[] colorArray = getColor(color);
		boolean blendingEnabled = (colorArray[3] < 1f);
		enableBlending(blendingEnabled);
		if (blendingEnabled) {
			GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2],
					colorArray[3]);
			checkError();
		}

		GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray,
				0);
		setPosition(mDrawParameters, offset);
		checkError();
	}

	private float[] getColor(int color) {
		float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha();
		float red = ((color >>> 16) & 0xFF) / 255f * alpha;
		float green = ((color >>> 8) & 0xFF) / 255f * alpha;
		float blue = (color & 0xFF) / 255f * alpha;
		mTempColor[0] = red;
		mTempColor[1] = green;
		mTempColor[2] = blue;
		mTempColor[3] = alpha;
		return mTempColor;
	}

	private void enableBlending(boolean enableBlending) {
		if (enableBlending) {
			GLES20.glEnable(GLES20.GL_BLEND);
			checkError();
		} else {
			GLES20.glDisable(GLES20.GL_BLEND);
			checkError();
		}
	}

	private void setPosition(ShaderParameter[] params, int offset) {
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates);
		checkError();
		GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle,
				COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, VERTEX_STRIDE,
				offset * VERTEX_STRIDE);
		checkError();
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
		checkError();
	}

	private void draw(ShaderParameter[] params, int type, int count, float x,
			float y, float width, float height) {
		setMatrix(params, x, y, width, height);
		int positionHandle = params[INDEX_POSITION].handle;
		GLES20.glEnableVertexAttribArray(positionHandle);
		checkError();
		GLES20.glDrawArrays(type, 0, count);
		checkError();
		GLES20.glDisableVertexAttribArray(positionHandle);
		checkError();
	}

	private void setMatrix(ShaderParameter[] params, float x, float y,
			float width, float height) {
		Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y,
				0f);
		Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
		Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0,
				mTempMatrix, 0);
		GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false,
				mTempMatrix, MATRIX_SIZE);
		checkError();
	}

	@Override
	public void fillRect(float x, float y, float width, float height, int color) {
		draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x,
				y, width, height, color, 0f);
		mCountFillRect++;
	}

	@Override
	public void drawTexture(BasicTexture texture, int x, int y, int width,
			int height) {
		if (width <= 0 || height <= 0) {
			return;
		}
		copyTextureCoordinates(texture, mTempSourceRect);
		mTempTargetRect.set(x, y, x + width, y + height);
		convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
		drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
	}

	private static void copyTextureCoordinates(BasicTexture texture,
			RectF outRect) {
		int left = 0;
		int top = 0;
		int right = texture.getWidth();
		int bottom = texture.getHeight();
		if (texture.hasBorder()) {
			left = 1;
			top = 1;
			right -= 1;
			bottom -= 1;
		}
		outRect.set(left, top, right, bottom);
	}

	@Override
	public void drawTexture(BasicTexture texture, RectF source, RectF target) {
		if (target.width() <= 0 || target.height() <= 0) {
			return;
		}
		mTempSourceRect.set(source);
		mTempTargetRect.set(target);

		convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
		drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
	}

	@Override
	public void drawTexture(BasicTexture texture, float[] textureTransform,
			int x, int y, int w, int h) {
		if (w <= 0 || h <= 0) {
			return;
		}
		mTempTargetRect.set(x, y, x + w, y + h);
		drawTextureRect(texture, textureTransform, mTempTargetRect);
	}

	private void drawTextureRect(BasicTexture texture, RectF source,
			RectF target) {
		setTextureMatrix(source);
		drawTextureRect(texture, mTempTextureMatrix, target);
	}

	private void setTextureMatrix(RectF source) {
		mTempTextureMatrix[0] = source.width();
		mTempTextureMatrix[5] = source.height();
		mTempTextureMatrix[12] = source.left;
		mTempTextureMatrix[13] = source.top;
	}

	// This function changes the source coordinate to the texture coordinates.
	// It also clips the source and target coordinates if it is beyond the
	// bound of the texture.
	private static void convertCoordinate(RectF source, RectF target,
			BasicTexture texture) {
		int width = texture.getWidth();
		int height = texture.getHeight();
		int texWidth = texture.getTextureWidth();
		int texHeight = texture.getTextureHeight();
		// Convert to texture coordinates
		source.left /= texWidth;
		source.right /= texWidth;
		source.top /= texHeight;
		source.bottom /= texHeight;

		// Clip if the rendering range is beyond the bound of the texture.
		float xBound = (float) width / texWidth;
		if (source.right > xBound) {
			target.right = target.left + target.width()
					* (xBound - source.left) / source.width();
			source.right = xBound;
		}
		float yBound = (float) height / texHeight;
		if (source.bottom > yBound) {
			target.bottom = target.top + target.height()
					* (yBound - source.top) / source.height();
			source.bottom = yBound;
		}
	}

	private void drawTextureRect(BasicTexture texture, float[] textureMatrix,
			RectF target) {
		ShaderParameter[] params = prepareTexture(texture);
		setPosition(params, OFFSET_FILL_RECT);
		GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1,
				false, textureMatrix, 0);
		checkError();
		if (texture.isFlippedVertically()) {
			save(SAVE_FLAG_MATRIX);
			translate(0, target.centerY());
			scale(1, -1, 1);
			translate(0, -target.centerY());
		}
		draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left,
				target.top, target.width(), target.height());
		if (texture.isFlippedVertically()) {
			restore();
		}
		mCountTextureRect++;
	}

	private ShaderParameter[] prepareTexture(BasicTexture texture) {
		ShaderParameter[] params;
		int program;
		if (texture.getTarget() == GLES20.GL_TEXTURE_2D) {
			params = mTextureParameters;
			program = mTextureProgram;
		} else {
			params = mOesTextureParameters;
			program = mOesTextureProgram;
		}
		prepareTexture(texture, program, params);
		return params;
	}

	private void prepareTexture(BasicTexture texture, int program,
			ShaderParameter[] params) {
		GLES20.glUseProgram(program);
		checkError();
		enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);
		GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
		checkError();
		texture.onBind(this);
		GLES20.glBindTexture(texture.getTarget(), texture.getId());
		checkError();
		GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0);
		checkError();
		GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha());
		checkError();
	}

	@Override
	public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer,
			int uvBuffer, int indexBuffer, int indexCount) {
		prepareTexture(texture, mMeshProgram, mMeshParameters);

		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
		checkError();

		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer);
		checkError();
		int positionHandle = mMeshParameters[INDEX_POSITION].handle;
		GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
				GLES20.GL_FLOAT, false, VERTEX_STRIDE, 0);
		checkError();

		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer);
		checkError();
		int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle;
		GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX,
				GLES20.GL_FLOAT, false, VERTEX_STRIDE, 0);
		checkError();
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
		checkError();

		GLES20.glEnableVertexAttribArray(positionHandle);
		checkError();
		GLES20.glEnableVertexAttribArray(texCoordHandle);
		checkError();

		setMatrix(mMeshParameters, x, y, 1, 1);
		GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount,
				GLES20.GL_UNSIGNED_BYTE, 0);
		checkError();

		GLES20.glDisableVertexAttribArray(positionHandle);
		checkError();
		GLES20.glDisableVertexAttribArray(texCoordHandle);
		checkError();
		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
		checkError();
		mCountDrawMesh++;
	}

	@Override
	public void drawMixed(BasicTexture texture, int toColor, float ratio,
			int x, int y, int w, int h) {
		copyTextureCoordinates(texture, mTempSourceRect);
		mTempTargetRect.set(x, y, x + w, y + h);
		drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect);
	}

	@Override
	public void drawMixed(BasicTexture texture, int toColor, float ratio,
			RectF source, RectF target) {
		if (target.width() <= 0 || target.height() <= 0) {
			return;
		}
		save(SAVE_FLAG_ALPHA);

		float currentAlpha = getAlpha();
		float cappedRatio = Math.min(1f, Math.max(0f, ratio));

		float textureAlpha = (1f - cappedRatio) * currentAlpha;
		setAlpha(textureAlpha);
		drawTexture(texture, source, target);

		float colorAlpha = cappedRatio * currentAlpha;
		setAlpha(colorAlpha);
		fillRect(target.left, target.top, target.width(), target.height(),
				toColor);

		restore();
	}

	@Override
	public boolean unloadTexture(BasicTexture texture) {
		boolean unload = texture.isLoaded();
		if (unload) {
			synchronized (mUnboundTextures) {
				mUnboundTextures.add(texture.getId());
			}
		}
		return unload;
	}

	@Override
	public void deleteBuffer(int bufferId) {
		synchronized (mUnboundTextures) {
			mDeleteBuffers.add(bufferId);
		}
	}

	@Override
	public void deleteRecycledResources() {
		synchronized (mUnboundTextures) {
			IntArray ids = mUnboundTextures;
			if (mUnboundTextures.size() > 0) {
				mGLId.glDeleteTextures(null, ids.size(),
						ids.getInternalArray(), 0);
				ids.clear();
			}

			ids = mDeleteBuffers;
			if (ids.size() > 0) {
				mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(),
						0);
				ids.clear();
			}
		}
	}

	@Override
	public void dumpStatisticsAndClear() {
		String line = String.format(
				"MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh,
				mCountTextureRect, mCountFillRect, mCountDrawLine);
		mCountDrawMesh = 0;
		mCountTextureRect = 0;
		mCountFillRect = 0;
		mCountDrawLine = 0;
		Log.d(TAG, line);
	}

	@Override
	public void endRenderTarget() {
		RawTexture oldTexture = mTargetTextures
				.remove(mTargetTextures.size() - 1);
		RawTexture texture = getTargetTexture();
		setRenderTarget(oldTexture, texture);
		restore(); // restore matrix and alpha
	}

	@Override
	public void beginRenderTarget(RawTexture texture) {
		save(); // save matrix and alpha and blending
		RawTexture oldTexture = getTargetTexture();
		mTargetTextures.add(texture);
		setRenderTarget(oldTexture, texture);
	}

	private RawTexture getTargetTexture() {
		return mTargetTextures.get(mTargetTextures.size() - 1);
	}

	private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) {
		if (oldTexture == null && texture != null) {
			GLES20.glGenFramebuffers(1, mFrameBuffer, 0);
			checkError();
			GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]);
			checkError();
		} else if (oldTexture != null && texture == null) {
			GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
			checkError();
			GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0);
			checkError();
		}

		if (texture == null) {
			setSize(mScreenWidth, mScreenHeight);
		} else {
			setSize(texture.getWidth(), texture.getHeight());

			if (!texture.isLoaded()) {
				texture.prepare(this);
			}

			GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
					GLES20.GL_COLOR_ATTACHMENT0, texture.getTarget(),
					texture.getId(), 0);
			checkError();

			checkFramebufferStatus();
		}
	}

	private static void checkFramebufferStatus() {
		int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
		if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
			String msg = "";
			switch (status) {
			case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
				msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT";
				break;
			case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
				msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
				break;
			case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
				msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";
				break;
			case GLES20.GL_FRAMEBUFFER_UNSUPPORTED:
				msg = "GL_FRAMEBUFFER_UNSUPPORTED";
				break;
			}
			throw new RuntimeException(msg + ":" + Integer.toHexString(status));
		}
	}

	@Override
	public void setTextureParameters(BasicTexture texture) {
		int target = texture.getTarget();
		GLES20.glBindTexture(target, texture.getId());
		checkError();
		GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S,
				GLES20.GL_CLAMP_TO_EDGE);
		GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T,
				GLES20.GL_CLAMP_TO_EDGE);
		GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER,
				GLES20.GL_LINEAR);
		GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER,
				GLES20.GL_LINEAR);
	}

	@Override
	public void initializeTextureSize(BasicTexture texture, int format, int type) {
		int target = texture.getTarget();
		GLES20.glBindTexture(target, texture.getId());
		checkError();
		int width = texture.getTextureWidth();
		int height = texture.getTextureHeight();
		GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type,
				null);
	}

	@Override
	public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
		int target = texture.getTarget();
		GLES20.glBindTexture(target, texture.getId());
		checkError();
		GLUtils.texImage2D(target, 0, bitmap, 0);
	}

	@Override
	public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
			Bitmap bitmap, int format, int type) {
		int target = texture.getTarget();
		GLES20.glBindTexture(target, texture.getId());
		checkError();
		GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
	}

	@Override
	public int uploadBuffer(FloatBuffer buf) {
		return uploadBuffer(buf, FLOAT_SIZE);
	}

	@Override
	public int uploadBuffer(ByteBuffer buf) {
		return uploadBuffer(buf, 1);
	}

	private int uploadBuffer(Buffer buffer, int elementSize) {
		mGLId.glGenBuffers(1, mTempIntArray, 0);
		checkError();
		int bufferId = mTempIntArray[0];
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
		checkError();
		GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity()
				* elementSize, buffer, GLES20.GL_STATIC_DRAW);
		checkError();
		return bufferId;
	}

	public static void checkError() {
		int error = GLES20.glGetError();
		if (error != 0) {
			Throwable t = new Throwable();
			Log.e(TAG, "GL error: " + error, t);
		}
	}

	@SuppressWarnings("unused")
	private static void printMatrix(String message, float[] m, int offset) {
		StringBuilder b = new StringBuilder(message);
		for (int i = 0; i < MATRIX_SIZE; i++) {
			b.append(' ');
			if (i % 4 == 0) {
				b.append('\n');
			}
			b.append(m[offset + i]);
		}
		Log.v(TAG, b.toString());
	}

	@Override
	public void recoverFromLightCycle() {
		GLES20.glViewport(0, 0, mWidth, mHeight);
		GLES20.glDisable(GLES20.GL_DEPTH_TEST);
		GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
		checkError();
	}

	@Override
	public void getBounds(Rect bounds, int x, int y, int width, int height) {
		Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y,
				0f);
		Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
		Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0,
				BOUNDS_COORDINATES, 0);
		Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0,
				BOUNDS_COORDINATES, 4);
		bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]);
		bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]);
		bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]);
		bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]);
		bounds.sort();
	}

	@Override
	public GLId getGLId() {
		return mGLId;
	}
}
